﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX.Direct3D9;
using System.Windows;
using System.Windows.Interop;
using SlimDX;
using System.Diagnostics;
using System.Windows.Media;
using System.Globalization;
using System.IO;
using System.Timers;
using System.Windows.Controls;
//using TransparencyEffect;
using System.Threading;

namespace Microsoft.Research.DynamicDataDisplay.DirectX2D
{
	public class DirectXHost : ContentControl, IPlotterElement, IDisposable
	{
        #region Public Properties
        public bool ShowFrameRate { get; set; }

        /// <summary>
        /// Gets or sets if the rendering needs to be updated.
        /// </summary>
        public bool RenderUpdate { get; set; }

        /// <summary>
        /// Gets the current DirectX device.
        /// </summary>
        public Device Device
        {
            get { return _device; }
        }

        /// <summary>
        /// Gets the current Direct3D object.
        /// </summary>
        public Direct3D Direct3D
        {
            get { return _direct3D; }
        } 
        #endregion

        #region Private Fields
        private bool _disposed = false;
        private Device _device;
        private Direct3D _direct3D;
        private D3DImage _image = new D3DImage();
        private bool _sizeChanged = false;
        private PresentParameters _pp = new PresentParameters();
        private HwndSource _hwnd;
        private Font _font;

        bool _initialized = false;
        bool _deviceLost = false;

        private Stopwatch _stopwatch = new Stopwatch();
        private double _frameCounter = 0;
        private long _frameRate = 0;
        TimeSpan _lastRender = new TimeSpan();

        private Color4 _backBufferColor = new Color4(1,0,0,0);
        #endregion

        #region Events
        /// <summary>
        /// Occurs when the main render loop is happening to give other controls a chance to use the device.
        /// </summary>
        public event EventHandler Render;

        /// <summary>
        /// Raises the Render event.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnRender(EventArgs e)
        {
            if (Render != null)
                Render(this, e);
        }

        /// <summary>
        /// Occurs when the device is lost.
        /// </summary>
        public event EventHandler DeviceLost;

        /// <summary>
        /// Raises the DeviceLost event.
        /// </summary>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected virtual void OnDeviceLost(EventArgs e)
        {
            if (DeviceLost != null)
                DeviceLost(this, e);
        }
        #endregion

        #region Constructors and Destructors
        public DirectXHost()
        {
            //Effect = new Transparency();

            IsVisibleChanged += new DependencyPropertyChangedEventHandler(DirectXHost_IsVisibleChanged);

            ImageBrush sceneBrush = new ImageBrush(_image);
            Grid contentGrid = new Grid();
            contentGrid.Background = sceneBrush;
            Content = contentGrid;

            _image.IsFrontBufferAvailableChanged += new DependencyPropertyChangedEventHandler(image_IsFrontBufferAvailableChanged);

#if DEBUG
            ShowFrameRate = true;
#else
            ShowFrameRate = false;
#endif
        }

        ~DirectXHost()
        {
            Dispose(false);
        }
        #endregion

        #region Public Methods
        public void AddDXChild(object child)
        {
            AddLogicalChild(child);
        }

        public void RemoveChild(object child)
        {
            RemoveLogicalChild(child);
        } 
        #endregion

        #region Private and Protected Methods
        /// <summary>
        /// Release unmanaged resources. 
        /// </summary>
        /// <param name="disposing"></param>
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this._disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    if (_font != null)
                    {
                        _font.Dispose();
                        _font = null;
                    }

                    if (_device != null)
                    {
                        _device.Dispose();
                        _device = null;
                    }

                    if (_direct3D != null)
                    {
                        _direct3D.Dispose();
                        _direct3D = null;
                    }
                }

            }
            _disposed = true;
        }

        private void DirectXHost_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (IsVisible)
                BeginRendering();
            else
                StopRendering();
        }

        private void image_IsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (_image.IsFrontBufferAvailable)
                BeginRendering();
            else
                StopRendering();
        }

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            if (!_initialized)
                Initialize3D();
        }

        /// <summary>
        /// Initializes the Direct3D object. If Direct3DEx is available, it should use that.
        /// </summary>
        private void Initialize3D()
        {
            if (_direct3D == null)
            {
                try
                {
                    var direct3DEx = new Direct3DEx();
                    _direct3D = direct3DEx;
                }
                catch
                {
                    _direct3D = new Direct3D();
                }

                if (_direct3D != null)
                {
                    CreateDevice();

                    _initialized = true;
                }
            }
        } 
        #endregion

        #region Device Methods
        /// <summary>
        /// Releases the Device.
        /// </summary>
        private void ReleaseDevice()
        {
            ReleaseUnmanagedResources();

            if (_device != null)
            {
                if (!_device.Disposed)
                {
                    _device.GetRenderTarget(0).Dispose(); // dispose the render target
                    _device.Dispose();
                    _device = null;
                }
            }

            _image.Lock();
            _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero);
            _image.Unlock();
        }

        /// <summary>
        /// Releases unmanaged resources used by DirectX.
        /// </summary>
        private void ReleaseUnmanagedResources()
        {
            if (_font != null)
            {
                if (!_font.Disposed)
                {
                    _font.Dispose();
                    _font = null;
                }
            }
        }

        /// <summary>
        /// Creates the device using the default presetation parameters.
        /// </summary>
        private void CreateDevice()
        {
            // first, we need to dispose the current device to release unmanaged resources
            if (_device != null)
            {
                ReleaseDevice();
            }

            try
            {
                _hwnd = new HwndSource(0, 0, 0, 0, 0, "D3", IntPtr.Zero);

                _pp = new PresentParameters();
                _pp.SwapEffect = SwapEffect.Discard;
                _pp.DeviceWindowHandle = _hwnd.Handle;
                _pp.Windowed = true;
                _pp.BackBufferWidth = (int)ActualWidth;
                _pp.BackBufferHeight = (int)ActualHeight;
                _pp.BackBufferFormat = Format.A8R8G8B8;

                if (_direct3D.GetType() == typeof(Direct3D))
                    _device = new Device(_direct3D, 0, DeviceType.Hardware, _hwnd.Handle, CreateFlags.MixedVertexProcessing |
                        CreateFlags.NoWindowChanges | CreateFlags.Multithreaded | CreateFlags.FpuPreserve, _pp);
                else if (_direct3D.GetType() == typeof(Direct3DEx))
                    _device = new DeviceEx(_direct3D as Direct3DEx, 0, DeviceType.Hardware, _hwnd.Handle, CreateFlags.MixedVertexProcessing |
                        CreateFlags.NoWindowChanges | CreateFlags.Multithreaded | CreateFlags.FpuPreserve, _pp);

                if (_device != null)
                {
                    _font = new Font(_device, 16, 0, SlimDX.Direct3D9.FontWeight.Bold, 0, false,
                        CharacterSet.Default, Precision.Default, FontQuality.Default, PitchAndFamily.DontCare, "Arial");
                }

                _image.Lock();
                _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _device.GetBackBuffer(0, 0).ComPointer);
                _image.Unlock();

                _deviceLost = false;
                Debug.WriteLine("Device created");
            }
            catch (Direct3D9Exception exc)
            {
                Debug.WriteLine("Error in CreateDevice() " + exc.Message);
            }
        }

        /// <summary>
        /// Resets the device by releasing and creating a new one. Had to do this to workaround Device.Reset not working.
        /// </summary>
        private void ResetDevice()
        {
            try
            {
                //Device.Reset(_pp); // this doesn't work - throws a D3DERR_INVALIDCALL error.
                Thread.Sleep(50); // wait a bit for DirectX to catch up
                CreateDevice();
            }
            catch (Direct3D9Exception exc)
            {
                Debug.WriteLine("Error in ResetDevice() " + exc.Message);
            }
        }
        #endregion

        #region Rendering Methods
        /// <summary>
        /// Starts the rendering loop.
        /// </summary>
        private void BeginRendering()
        {
            if (!_initialized)
                Initialize3D();

            if (_image.IsFrontBufferAvailable && _initialized)
            {
                CompositionTarget.Rendering += CompositionTarget_Rendering;
                Debug.WriteLine("BeginRendering");

                if (!_stopwatch.IsRunning)
                    _stopwatch.Start();
            }
        }

        /// <summary>
        /// Stops the rendering loop.
        /// </summary>
        private void StopRendering()
        {
            CompositionTarget.Rendering -= CompositionTarget_Rendering;
            Debug.WriteLine("StopRendering");

            if (_stopwatch.IsRunning)
                _stopwatch.Stop();
        }

        /// <summary>
        /// Provides the main rendering loop by tapping into the WPF render loop.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            if (IsVisible)
            {
                RenderingEventArgs args = (RenderingEventArgs)e;

                // only update if some amount of time has passed to avoid multiple calls in the same frame.
                if (_lastRender != args.RenderingTime)
                {
                    BeginScene();
                    _lastRender = args.RenderingTime;
                }
            }
        }

        /// <summary>
        /// Begins the rendering process.
        /// </summary>
        private void BeginScene()
        {
            if (_image == null) return;
            if (!IsVisible) return;

            // handle the device being null
            if (_device == null)
            {
                StopRendering();
                CreateDevice();
                return;
            }

            // handle the device being lost
            if (_deviceLost)
            {
                ResetDevice();
            }

            // handle the size of our surface changing
            if (_sizeChanged)
            {
                _pp.BackBufferWidth = (int)ActualWidth;
                _pp.BackBufferHeight = (int)ActualHeight;

                CreateDevice();

                Viewport vp = new Viewport();
                vp.MinZ = 0.0f;
                vp.MaxZ = 1.0f;
                vp.Width = (int)ActualWidth;
                vp.Height = (int)ActualHeight;
                _device.Viewport = vp;
                _sizeChanged = false;
            }

            // finaly, try to render the scene.
            if (_image.IsFrontBufferAvailable)
            {
                try
                {
                    _image.Lock();
                    if (RenderScene())
                    {
                        _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _device.GetBackBuffer(0, 0).ComPointer);
                        _image.AddDirtyRect(new Int32Rect(0, 0, _image.PixelWidth, _image.PixelHeight));
                        
                    }
                    _image.Unlock();
                }
                catch (Exception exc)
                {
                    Debug.WriteLine(exc.Message);
                }
            }
        }

        public bool RenderScene()
        {
            if (_device == null) return false;

            bool result = false;
            Result res;

            if (ShowFrameRate)
            {
                _frameCounter++;
                _frameRate = (long)(_frameCounter / this._stopwatch.Elapsed.TotalSeconds);
            }

            try
            {
                // if the device was lost last time, check to see if we have it back yet
                if (_deviceLost)
                {
                    res = Device.TestCooperativeLevel();

                    if (res == SlimDX.Direct3D9.ResultCode.DeviceLost)
                        return false;
                }

                // only render if we have to
                if (RenderUpdate)
                {
                    _device.SetRenderState(SlimDX.Direct3D9.RenderState.CullMode, Cull.None);
                    _device.SetRenderState(SlimDX.Direct3D9.RenderState.ZEnable, true);
                    _device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, _backBufferColor, 1.0f, 1);
                    //device.Clear(ClearFlags.Target, new Color4(1, 0, 0, 0), 1.0f, 0);
                    _device.BeginScene();

                    try
                    {
                        //Render.Raise(this);
                        OnRender(EventArgs.Empty);
                    }
                    catch (Exception exc)
                    {
                        Debug.WriteLine("Error in rendering in DirectXHost: " + exc.Message);
                    }

                    if (ShowFrameRate) // draw the framerate
                        _font.DrawString(null, _frameRate.ToString(), 25, 25, Colors.Red.ToArgb());

                    _device.EndScene();
                    //_device.Present();

                    RenderUpdate = false;
                    result = true;
                }

            }
            catch (Direct3D9Exception exc)
            {
                //Device.Reset(pp);

                // handle the device being last and let others know.
                if (exc.ResultCode.Name == "D3DERR_DEVICELOST")
                {
                    OnDeviceLost(EventArgs.Empty);
                    _deviceLost = true;
                }
                Debug.WriteLine("Exception in main render loop: " + exc.Message);
            }

            return result;
        } 
        #endregion


		#region IPlotterElement Members

		private Plotter2D plotter;
		public void OnPlotterAttached(Plotter plotter)
		{
			this.plotter = (Plotter2D)plotter;
			plotter.CentralGrid.Children.Add(this);
			this.plotter.Viewport.PropertyChanged += Viewport_PropertyChanged;
		}

		private void Viewport_PropertyChanged(object sender, ExtendedPropertyChangedEventArgs e)
		{
			if (e.PropertyName == "Output")
			{
				_sizeChanged = true;
                
			}
		}

		public void OnPlotterDetaching(Plotter plotter)
		{
			this.plotter.Viewport.PropertyChanged -= Viewport_PropertyChanged;
			plotter.CentralGrid.Children.Remove(this);
			this.plotter = null;
		}

		public Plotter Plotter
		{
			get { return plotter; }
		}

		#endregion


        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }

        #endregion
    }
}
